/*
* Copyright (c) 2008. All rights reserved.
*/
package ro.isdc.wro.util;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import junit.framework.AssertionFailedError;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.FalseFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.apache.commons.lang3.Validate;
import org.junit.Assert;
import org.junit.ComparisonFailure;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.isdc.wro.WroRuntimeException;
import ro.isdc.wro.config.Context;
import ro.isdc.wro.manager.factory.BaseWroManagerFactory;
import ro.isdc.wro.manager.factory.WroManagerFactory;
import ro.isdc.wro.model.WroModel;
import ro.isdc.wro.model.factory.WroModelFactory;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.group.processor.InjectorBuilder;
import ro.isdc.wro.model.resource.Resource;
import ro.isdc.wro.model.resource.ResourceType;
import ro.isdc.wro.model.resource.locator.UriLocator;
import ro.isdc.wro.model.resource.locator.factory.AbstractUriLocatorFactory;
import ro.isdc.wro.model.resource.locator.factory.DefaultUriLocatorFactory;
import ro.isdc.wro.model.resource.locator.factory.UriLocatorFactory;
import ro.isdc.wro.model.resource.processor.ResourcePostProcessor;
import ro.isdc.wro.model.resource.processor.ResourcePreProcessor;
import ro.isdc.wro.model.resource.processor.decorator.ProcessorDecorator;
import ro.isdc.wro.model.resource.processor.factory.SimpleProcessorsFactory;
/**
* WroTestUtils.
*
* @author Alex Objelean
* @created Created on Nov 28, 2008
*/
public class WroTestUtils {
private static final Logger LOG = LoggerFactory.getLogger(WroTestUtils.class);
/**
* @return a {@link BaseWroManagerFactory} which uses an empty model.
*/
public static BaseWroManagerFactory simpleManagerFactory() {
return new BaseWroManagerFactory().setModelFactory(simpleModelFactory(new WroModel()));
}
/**
* Compare contents of two resources (files) by performing some sort of processing on input resource.
*
* @param inputResourceUri
* uri of the resource to process.
* @param expectedContentResourceUri
* uri of the resource to compare with processed content.
* @param processor
* a closure used to process somehow the input content.
*/
public static void compareProcessedResourceContents(final String inputResourceUri,
final String expectedContentResourceUri, final ResourcePostProcessor processor)
throws IOException {
final Reader resultReader = getReaderFromUri(inputResourceUri);
final Reader expectedReader = getReaderFromUri(expectedContentResourceUri);
compare(resultReader, expectedReader, processor);
}
private static Reader getReaderFromUri(final String uri)
throws IOException {
// wrap reader with bufferedReader for top efficiency
return new BufferedReader(new InputStreamReader(createDefaultUriLocatorFactory().locate(uri)));
}
private static UriLocatorFactory createDefaultUriLocatorFactory() {
return new DefaultUriLocatorFactory();
}
public static InputStream getInputStream(final String uri)
throws IOException {
return createDefaultUriLocatorFactory().locate(uri);
}
public static void init(final WroModelFactory factory) {
final WroManagerFactory managerFactroy = new BaseWroManagerFactory().setModelFactory(factory);
InjectorBuilder.create(managerFactroy).build().inject(factory);
}
/**
* @return the injected processor.
*/
public static ResourcePreProcessor initProcessor(final ResourcePreProcessor processor) {
final BaseWroManagerFactory factory = new BaseWroManagerFactory();
factory.setProcessorsFactory(new SimpleProcessorsFactory().addPreProcessor(processor));
final Injector injector = InjectorBuilder.create(factory).build();
injector.inject(processor);
return processor;
}
/**
* @return the injector
*/
public static void initProcessor(final ResourcePostProcessor processor) {
initProcessor((ResourcePreProcessor) new ProcessorDecorator(processor));
}
/**
* Compare contents of two resources (files) by performing some sort of processing on input resource.
*
* @param inputResourceUri
* uri of the resource to process.
* @param expectedContentResourceUri
* uri of the resource to compare with processed content.
* @param processor
* a closure used to process somehow the input content.
*/
public static void compare(final Reader resultReader, final Reader expectedReader,
final ResourcePostProcessor processor)
throws IOException {
final Writer resultWriter = new StringWriter();
processor.process(resultReader, resultWriter);
final Writer expectedWriter = new StringWriter();
IOUtils.copy(expectedReader, expectedWriter);
compare(expectedWriter.toString(), resultWriter.toString());
expectedReader.close();
expectedWriter.close();
}
public static void compare(final InputStream input, final InputStream expected, final ResourcePostProcessor processor)
throws IOException {
compare(new InputStreamReader(input), new InputStreamReader(expected), processor);
}
/**
* Compare if content of expected stream is the same as content of the actual stream. When compared content is not
* equal, the assertion error will be thrown.
*
* @param expected
* {@link InputStream} of the expected content.
* @param actual
* {@link InputStream} of the actual content.
*/
public static void compare(final InputStream expected, final InputStream actual)
throws IOException {
Assert.assertNotNull(expected);
Assert.assertNotNull(actual);
final String encoding = Context.get().getConfig().getEncoding();
compare(IOUtils.toString(expected, encoding), IOUtils.toString(actual, encoding));
expected.close();
actual.close();
}
/**
* Compares two strings by removing trailing spaces & tabs for correct comparison.
*/
public static void compare(final String expected, final String actual) {
try {
final String in = replaceTabsWithSpaces(expected.trim());
final String out = replaceTabsWithSpaces(actual.trim());
Assert.assertEquals(in, out);
LOG.debug("Compare.... [OK]");
} catch (final ComparisonFailure e) {
LOG.error("Compare.... [FAIL]", e.getMessage());
throw e;
}
}
/**
* Replace tabs with spaces.
*
* @param input
* from where to remove tabs.
* @return cleaned string.
*/
private static String replaceTabsWithSpaces(final String input) {
// replace tabs with spaces
return input.replaceAll("\\t", " ").replaceAll("\\r", "");
}
public static void compareFromSameFolder(final File sourceFolder, final IOFileFilter sourceFileFilter,
final Transformer<String> toTargetFileName, final ResourcePreProcessor processor) {
final Collection<File> files = FileUtils.listFiles(sourceFolder, sourceFileFilter, FalseFileFilter.INSTANCE);
int processedNumber = 0;
for (final File file : files) {
LOG.debug("processing: {}", file.getName());
File targetFile = null;
try {
targetFile = new File(sourceFolder, toTargetFileName.transform(file.getName()));
final InputStream targetFileStream = new FileInputStream(targetFile);
LOG.debug("comparing with: {}", targetFile.getName());
try {
compare(new FileInputStream(file), targetFileStream, new ResourcePostProcessor() {
public void process(final Reader reader, final Writer writer)
throws IOException {
// ResourceType doesn't matter here
processor.process(Resource.create("file:" + file.getPath(), ResourceType.CSS), reader, writer);
}
});
} catch (final ComparisonFailure e) {
LOG.error("Failed comparing: {}", file.getName());
throw e;
}
processedNumber++;
} catch (final IOException e) {
LOG.debug("Skip comparison because couldn't find the TARGET file {}. Original cause: {}", targetFile,
e.getCause());
} catch (final Exception e) {
throw WroRuntimeException.wrap(e);
}
}
logSuccess(processedNumber);
}
private static void logSuccess(final int size) {
if (size == 0) {
throw new IllegalStateException("No files compared. Check if there is at least one resource to compare");
}
LOG.debug("===============");
LOG.debug("Successfully processed: {} files.", size);
LOG.debug("===============");
}
/**
* Process and compare all the files from the sourceFolder and compare them with the files from the targetFolder.
*/
public static void compareFromDifferentFolders(final File sourceFolder, final File targetFolder,
final ResourcePreProcessor processor)
throws IOException {
compareFromDifferentFolders(sourceFolder, targetFolder, TrueFileFilter.TRUE, Transformers.noOpTransformer(),
processor);
}
public static void compareFromDifferentFoldersByExtension(final File sourceFolder, final File targetFolder,
final String extension, final ResourcePreProcessor processor)
throws IOException {
compareFromDifferentFolders(sourceFolder, targetFolder, new WildcardFileFilter("*." + extension),
Transformers.noOpTransformer(), processor);
}
/**
* TODO run tests in parallel
*/
public static void compareFromDifferentFoldersByExtension(final File sourceFolder, final File targetFolder,
final String extension, final ResourcePostProcessor processor)
throws IOException {
compareFromDifferentFolders(sourceFolder, targetFolder, new WildcardFileFilter("*." + extension),
Transformers.noOpTransformer(), processor);
}
/**
* Compares files with the same name from sourceFolder against it's counterpart in targetFolder, but allows source and
* target files to have different extensions. TODO run tests in parallel
*/
public static void compareFromDifferentFoldersByName(final File sourceFolder, final File targetFolder,
final String srcExtension, final String targetExtension, final ResourcePostProcessor processor)
throws IOException {
compareFromDifferentFolders(sourceFolder, targetFolder, new WildcardFileFilter("*." + srcExtension),
Transformers.extensionTransformer("css"), processor);
}
private static void compareFromDifferentFolders(final File sourceFolder, final File targetFolder,
final IOFileFilter fileFilter, final Transformer<String> toTargetFileName, final ResourcePostProcessor processor)
throws IOException {
// TODO use ProcessorsUtils
compareFromDifferentFolders(sourceFolder, targetFolder, fileFilter, toTargetFileName, new ResourcePreProcessor() {
public void process(final Resource resource, final Reader reader, final Writer writer)
throws IOException {
processor.process(reader, writer);
}
});
}
/**
* Applies a function for each file from a folder. The folder should contain at least one file to process, otherwise
* an exception will be thrown.
*
* @param folder
* {@link File} representing the folder where the files will be used from processing.
* @param function
* {@link Function} to apply on each found file.
*/
public static void forEachFileInFolder(final File folder, final Function<File, Void> function) {
Validate.notNull(function);
final Collection<File> files = FileUtils.listFiles(folder, TrueFileFilter.TRUE, FalseFileFilter.INSTANCE);
int processedNumber = 0;
for (final File file : files) {
try {
function.apply(file);
} catch (final Exception e) {
throw new RuntimeException("Problem while applying function on file: " + file, e);
}
processedNumber++;
}
logSuccess(processedNumber);
}
/**
* Process and compare the files which a located in different folders.
*
* @param sourceFolder
* folder where the source files are located.
* @param targetFolder
* folder where the target files are located.
* @param fileFilter
* filter used to select files to process.
* @param toTargetFileName
* {@link Transformer} used to identify the target file name based on source file name.
* @param preProcessor
* {@link ResourcePreProcessor} used to process the source files.
* @throws IOException
*/
private static void compareFromDifferentFolders(final File sourceFolder, final File targetFolder,
final IOFileFilter fileFilter, final Transformer<String> toTargetFileName, final ResourcePreProcessor preProcessor)
throws IOException {
LOG.debug("sourceFolder: {}", sourceFolder);
LOG.debug("targetFolder: {}", targetFolder);
final Collection<File> files = FileUtils.listFiles(sourceFolder, fileFilter, FalseFileFilter.INSTANCE);
int processedNumber = 0;
// TODO use WroUtil#runInParallel for running tests faster
for (final File file : files) {
File targetFile = null;
try {
targetFile = new File(targetFolder, toTargetFileName.transform(file.getName()));
final InputStream targetFileStream = new FileInputStream(targetFile);
LOG.debug("=========== processing: {} ===========", file.getName());
// ResourceType doesn't matter here
try {
compare(new FileInputStream(file), targetFileStream, new ResourcePostProcessor() {
public void process(final Reader reader, final Writer writer)
throws IOException {
// ResourceType doesn't matter here
ResourceType resourceType = ResourceType.CSS;
try {
resourceType = ResourceType.get(FilenameUtils.getExtension(file.getPath()));
} catch (final IllegalArgumentException e) {
LOG.warn("unkown resource type for file: {}, assuming resource type is: {}", file.getPath(),
resourceType);
}
try {
preProcessor.process(Resource.create("file:" + file.getPath(), resourceType), reader, writer);
} catch (final Exception e) {
LOG.error("processing failed...", e);
throw WroRuntimeException.wrap(e, "Processing failed...");
}
}
});
} catch (final ComparisonFailure e) {
LOG.error("failed comparing: {}", file.getName());
throw e;
}
processedNumber++;
} catch (final IOException e) {
LOG.debug("Skip comparison because couldn't find the TARGET file {}\n. Original exception: {}", targetFile,
e.getCause());
} catch (final Exception e) {
throw WroRuntimeException.wrap(e, "A problem during transformation occured");
}
}
logSuccess(processedNumber);
}
/**
* Runs a task concurrently. Allows to test thread-safe behavior.
*
* @param task
* a {@link Callable} to run concurrently.
* @throws Exception
* if any of the executed tasks fails.
*/
public static void runConcurrently(final Callable<Void> task, final int times)
throws Exception {
final ExecutorService service = Executors.newFixedThreadPool(5);
final List<Future<?>> futures = new ArrayList<Future<?>>();
for (int i = 0; i < times; i++) {
futures.add(service.submit(task));
}
for (final Future<?> future : futures) {
future.get();
}
}
public static void runConcurrently(final Callable<Void>... tasks) throws Exception {
final ExecutorService service = Executors.newFixedThreadPool(5);
final List<Future<?>> futures = new ArrayList<Future<?>>();
for (final Callable<Void> task : tasks) {
futures.add(service.submit(task));
}
for (final Future<?> future : futures) {
future.get();
}
}
/**
* Run the task concurrently 50 times.
*/
public static void runConcurrently(final Callable<Void> task)
throws Exception {
runConcurrently(task, 50);
}
/**
* A blocking operation which waits for the provided function to be true. If the function doesn't return true after a
* timeout period (expressed in milliseconds), it will throw a {@link RuntimeException}.
*
* @param function
* a {@link Function} responsible for expressing the until expression. It must return true if the wait
* blocking operation should stop.
* @param timeout
* number of milliseconds to wait until expression is true. If this timeout is exceeded, the exception will
* be thrown.
*/
public static void waitUntil(final Function<Void, Boolean> function, final long timeout) {
final long start = System.currentTimeMillis();
try {
while (!function.apply(null)) {
if (System.currentTimeMillis() - start > timeout) {
throw new WroRuntimeException("Timeout of " + timeout + "ms exceeded.");
}
}
} catch (final Exception e) {
throw WroRuntimeException.wrap(e);
}
}
/**
* @return a default {@link Injector} to be used by test classes.
*/
public static Injector createInjector() {
return InjectorBuilder.create(new BaseWroManagerFactory()).build();
}
/**
* Creates a model factory for a given model.
*/
public static WroModelFactory simpleModelFactory(final WroModel model) {
Validate.notNull(model);
return new WroModelFactory() {
public WroModel create() {
return model;
}
public void destroy() {
}
};
}
/**
* Asserts that a processor supports provided resource types.
*/
public static void assertProcessorSupportResourceTypes(final ResourcePreProcessor processor,
final ResourceType... expectedResourceTypes) {
final ResourceType[] actualResourceTypes = new ProcessorDecorator(processor).getSupportedResourceTypes();
try {
Assert.assertTrue(Arrays.equals(expectedResourceTypes, actualResourceTypes));
} catch (final AssertionFailedError e) {
final String message = "actual resourceTypes: " + Arrays.toString(actualResourceTypes) + ", expected are: "
+ Arrays.toString(expectedResourceTypes);
LOG.error(message);
Assert.fail(message);
}
}
/**
* @return an implementation of {@link UriLocatorFactory} which always return a valid stream which contains the
* resource uri as content.
*/
public static UriLocatorFactory createResourceMockingLocatorFactory() {
return new AbstractUriLocatorFactory() {
public UriLocator getInstance(final String uri) {
return createResourceMockingLocator();
}
};
}
/**
* @return an implementation of {@link UriLocator} which always return a valid stream which contains the resource uri
* as content.
*/
public static UriLocator createResourceMockingLocator() {
return new UriLocator() {
public InputStream locate(final String uri)
throws IOException {
return new ByteArrayInputStream(uri.getBytes());
}
public boolean accept(final String uri) {
return true;
}
};
}
}